package in.oneton.idea.spring.assistant.plugin.misc;

import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.codeInsight.daemon.impl.analysis.JavaGenericsUtil;
import com.intellij.codeInsight.javadoc.JavaDocInfoGenerator;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiArrayType;
import com.intellij.psi.PsiCapturedWildcardType;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMember;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiPrimitiveType;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeParameter;
import com.intellij.psi.PsiWildcardType;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.util.CachedValue;
import com.intellij.psi.util.PropertyUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.TimeoutUtil;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import gnu.trove.TObjectHashingStrategy;
import in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType;
import in.oneton.idea.spring.assistant.plugin.suggestion.clazz.GenericClassMemberWrapper;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;

import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

import static com.intellij.openapi.module.ModuleUtilCore.findModuleForFile;
import static com.intellij.openapi.module.ModuleUtilCore.findModuleForPsiElement;
import static com.intellij.openapi.roots.ModuleRootManager.getInstance;
import static com.intellij.openapi.util.Key.create;
import static com.intellij.psi.CommonClassNames.JAVA_LANG_ITERABLE;
import static com.intellij.psi.CommonClassNames.JAVA_LANG_STRING;
import static com.intellij.psi.CommonClassNames.JAVA_UTIL_MAP;
import static com.intellij.psi.JavaPsiFacade.getElementFactory;
import static com.intellij.psi.PsiModifier.PUBLIC;
import static com.intellij.psi.PsiModifier.STATIC;
import static com.intellij.psi.PsiType.NULL;
import static com.intellij.psi.util.CachedValueProvider.Result.create;
import static com.intellij.psi.util.CachedValuesManager.getCachedValue;
import static com.intellij.psi.util.InheritanceUtil.isInheritor;
import static com.intellij.psi.util.PropertyUtil.getPropertyName;
import static com.intellij.psi.util.PropertyUtil.isSimplePropertyGetter;
import static com.intellij.psi.util.PropertyUtil.isSimplePropertySetter;
import static com.intellij.psi.util.PsiModificationTracker.JAVA_STRUCTURE_MODIFICATION_COUNT;
import static com.intellij.psi.util.PsiTypesUtil.getClassType;
import static com.intellij.psi.util.PsiTypesUtil.hasUnresolvedComponents;
import static com.intellij.psi.util.PsiUtil.extractIterableTypeParameter;
import static com.intellij.psi.util.PsiUtil.resolveGenericsClassInType;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNode.sanitise;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.ARRAY;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.BOOLEAN;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.BYTE;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.CHAR;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.DOUBLE;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.ENUM;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.FLOAT;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.INT;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.ITERABLE;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.KNOWN_CLASS;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.LONG;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.MAP;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.SHORT;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.STRING;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.UNDEFINED;
import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.UNKNOWN_CLASS;
import static java.util.Objects.requireNonNull;

@UtilityClass
public class PsiCustomUtil {
  private static final Logger log = Logger.getInstance(PsiCustomUtil.class);

  private static final Key<CachedValue<Map<String, GenericClassMemberWrapper>>>
      SPRING_ASSISTANT_PLUGIN_PROPERTY_TO_CLASS_MEMBER_WRAPPER_KEY =
      create("spring_assistant_plugin_property_to_class_member_wrapper");
  private static final Key<CachedValue<PsiType>>
      SPRING_ASSISTANT_PLUGIN_ERASE_FREE_TYPE_PARAMETER_TYPE_KEY =
      create("spring_assistant_plugin_eraseFreeTypeParameterType");
  private static final Key<CachedValue<PsiType>> SPRING_ASSISTANT_PLUGIN_FIRST_PARAMETER_TYPE_KEY =
      create("spring_assistant_plugin_firstParameterType");
  private static final Key<CachedValue<PsiType>> SPRING_ASSISTANT_PLUGIN_RETURN_TYPE_KEY =
      create("spring_assistant_plugin_returnType");

  @Nullable
  public static PsiType safeGetValidType(@NotNull Module module, @NotNull String fqn) {
    try {
      // Intellij expects inner classes to be referred via `.` instead of `$`
      PsiType type = JavaPsiFacade.getInstance(module.getProject()).getParserFacade()
          .createTypeFromText(fqn.replaceAll("\\$", "."), null);
      boolean typeValid = isValidType(type);
      if (typeValid) {
        if (type instanceof PsiClassType) {
          return PsiClassType.class.cast(type);
        } else if (type instanceof PsiArrayType) {
          return PsiArrayType.class.cast(type);
        }
      }
      return null;
    } catch (IncorrectOperationException e) {
      debug(() -> log.debug("Unable to find class fqn " + fqn));
      return null;
    }
  }

  @NotNull
  public static PsiType getReferredPsiType(PsiElement psiElement) {
    if (psiElement instanceof PsiField) {
      return ((PsiField) psiElement).getType();
    } else if (psiElement instanceof PsiMethod) {
      return requireNonNull(((PsiMethod) psiElement).getReturnType());
    } else if (psiElement instanceof PsiClass) {
      return getClassType((PsiClass) psiElement);
    }
    throw new RuntimeException(
        "Method supports psiElement of type PsiField, PsiMethod & PsiClass only");
  }

  @Nullable
  public static Map<PsiTypeParameter, PsiType> getTypeParameters(@NotNull PsiElement psiElement) {
    PsiType psiType = getReferredPsiType(psiElement);
    return getTypeParameters(psiType);
  }

  @Nullable
  public static Map<PsiTypeParameter, PsiType> getTypeParameters(PsiType type) {
    if (type instanceof PsiArrayType) {
      return getTypeParameters(((PsiArrayType) type).getComponentType());
    } else if (type instanceof PsiPrimitiveType) {
      return null;
    } else if (type instanceof PsiClassType) {
      PsiClassType.ClassResolveResult resolveResult =
          PsiClassType.class.cast(type).resolveGenerics();
      if (resolveResult.isValidResult()) {
        return resolveResult.getSubstitutor().getSubstitutionMap();
      }
    }
    return null;
  }

  @NotNull
  public static SuggestionNodeType getSuggestionNodeType(PsiType type) {
    if (type == null) {
      return UNDEFINED;
    } else if (type instanceof PsiArrayType) {
      return ARRAY;
    } else if (type instanceof PsiPrimitiveType) {
      SuggestionNodeType nodeType = getSuggestionNodeTypeForPrimitive(type);
      return nodeType != null ? nodeType : UNKNOWN_CLASS;
    } else if (type instanceof PsiClassType) {
      SuggestionNodeType nodeType = getSuggestionNodeTypeForPrimitive(type);
      if (nodeType != null) {
        return nodeType;
      } else if (type.getCanonicalText().equals(JAVA_LANG_STRING)) {
        return STRING;
      }

      // TODO: Need to check if this is required or not?
      PsiClassType psiClassType = (PsiClassType) type;
      PsiClassType.ClassResolveResult classResolveResult = psiClassType.resolveGenerics();
      if (classResolveResult.isValidResult()) {
        PsiClass psiClass = requireNonNull(classResolveResult.getElement());
        if (psiClass.isEnum()) {
          return ENUM;
        } else if ("java.math.BigDecimal".equals(psiClass.getQualifiedName())) {
          return DOUBLE;
        } else if ("java.nio.charset.Charset".equals(psiClass.getQualifiedName())
            // charset is a string
            || "org.springframework.http.MediaType".equals(psiClass.getQualifiedName())
            || "java.net.InetAddress".equals(psiClass.getQualifiedName()) // ip address or hostname
            || "java.net.URI".equals(psiClass.getQualifiedName()) // url
            || requireNonNull(psiClass.getQualifiedName()).startsWith("java.lang.Class")
            // expecting class name
            || "org.springframework.core.io.Resource".equals(psiClass.getQualifiedName())
            // spring resource such as classpath://myfile.json, file:///opt/myfile.json
            || "java.util.Date".equals(psiClass.getQualifiedName()) || "java.sql.Date"
            .equals(psiClass.getQualifiedName()) || "java.time.LocalDate"
            .equals(psiClass.getQualifiedName()) || "java.time.LocalTime"
            .equals(psiClass.getQualifiedName()) || "java.time.LocalDateTime"
            .equals(psiClass.getQualifiedName()) || "java.time.ZoneId"
            .equals(psiClass.getQualifiedName()) || "java.time.ZonedDateTime"
            .equals(psiClass.getQualifiedName()) || "java.time.Instant"
            .equals(psiClass.getQualifiedName()) || "java.time.Duration"
            .equals(psiClass.getQualifiedName())// date & duation
            || "java.time.Period".equals(psiClass.getQualifiedName()) || "java.util.Locale"
            .equals(psiClass.getQualifiedName())) {
          return STRING;
        } else if (isMap(psiClass)) {
          return MAP;
        } else if (isIterable(psiClass)) {
          return ITERABLE;
        } else {
          return KNOWN_CLASS;
        }
      }
    }
    return UNKNOWN_CLASS;
  }

  @Nullable
  private static SuggestionNodeType getSuggestionNodeTypeForPrimitive(PsiType type) {
    if (PsiType.BOOLEAN.equals(type) || PsiType.BOOLEAN
        .equals(PsiPrimitiveType.getUnboxedType(type))) {
      return BOOLEAN;
    } else if (PsiType.BYTE.equals(type) || PsiType.BYTE
        .equals(PsiPrimitiveType.getUnboxedType(type))) {
      return BYTE;
    } else if (PsiType.SHORT.equals(type) || PsiType.SHORT
        .equals(PsiPrimitiveType.getUnboxedType(type))) {
      return SHORT;
    } else if (PsiType.INT.equals(type) || PsiType.INT
        .equals(PsiPrimitiveType.getUnboxedType(type))) {
      return INT;
    } else if (PsiType.LONG.equals(type) || PsiType.LONG
        .equals(PsiPrimitiveType.getUnboxedType(type))) {
      return LONG;
    } else if (PsiType.FLOAT.equals(type) || PsiType.FLOAT
        .equals(PsiPrimitiveType.getUnboxedType(type))) {
      return FLOAT;
    } else if (PsiType.DOUBLE.equals(type) || PsiType.DOUBLE
        .equals(PsiPrimitiveType.getUnboxedType(type))) {
      return DOUBLE;
    } else if (PsiType.CHAR.equals(type) || PsiType.CHAR
        .equals(PsiPrimitiveType.getUnboxedType(type))) {
      return CHAR;
    }
    return null;
  }

  @Nullable
  public static String toClassFqn(@NotNull PsiType type) {
    if (type instanceof PsiArrayType) {
      String componentLongName = toClassFqn(PsiArrayType.class.cast(type).getComponentType());
      if (componentLongName != null) {
        return componentLongName + "[]";
      }
    } else if (type instanceof PsiPrimitiveType) {
      return type.getPresentableText();
    } else if (type instanceof PsiClassType) {
      return type.getCanonicalText();
    }
    return null;
  }

  @Nullable
  public static String toClassNonQualifiedName(@NotNull PsiType type) {
    if (type instanceof PsiArrayType) {
      String componentLongName =
          toClassNonQualifiedName(PsiArrayType.class.cast(type).getComponentType());
      if (componentLongName != null) {
        return componentLongName + "[]";
      }
    } else if (type instanceof PsiPrimitiveType) {
      return type.getPresentableText();
    } else if (type instanceof PsiClassType) {
      return ((PsiClassType) type).getClassName();
    }
    return null;
  }

  private static boolean isMap(@NotNull PsiClass psiClass) {
    return isClassSameOrDescendantOf(psiClass, JAVA_UTIL_MAP) || isClassSameOrDescendantOf(psiClass,
        "java.util.Hashtable");
  }

  private static boolean isIterable(@NotNull PsiClass psiClass) {
    return isClassSameOrDescendantOf(psiClass, JAVA_LANG_ITERABLE);
  }

  private static boolean isClassSameOrDescendantOf(@NotNull PsiClass psiClass,
      String expectedClassFqn) {
    return psiClass.getQualifiedName() != null && isInheritor(psiClass, expectedClassFqn);
  }

  @NotNull
  public static PsiType getBoxedTypeFromPrimitiveType(Module module,
      PsiPrimitiveType primitiveType) {
    PsiType boxedPrimitiveType = safeGetValidType(module, primitiveType.getBoxedTypeName());
    assert boxedPrimitiveType instanceof PsiClassType;
    return boxedPrimitiveType;
  }

  @Nullable
  public static String typeToFqn(Module module, @NotNull PsiType type) {
    if (isValidType(type)) {
      if (type instanceof PsiArrayType) {
        type = PsiArrayType.class.cast(type).getComponentType();
        return type.getCanonicalText();
      } else if (type instanceof PsiPrimitiveType) {
        return getBoxedTypeFromPrimitiveType(module, (PsiPrimitiveType) type).getCanonicalText();
      } else if (type instanceof PsiClassType) {
        return type.getCanonicalText();
      }
    }
    return null;
  }

  @Nullable
  public static Set<PsiClass> computeDependencies(Module module, @NotNull PsiType type) {
    PsiType originalType = type;
    if (isValidType(type)) {
      if (type instanceof PsiArrayType) {
        return computeDependencies(module, PsiArrayType.class.cast(type).getComponentType());
      } else if (type instanceof PsiPrimitiveType) {
        type = getBoxedTypeFromPrimitiveType(module, (PsiPrimitiveType) type);
      } else if (type instanceof PsiWildcardType) {
        type = ((PsiWildcardType) type).getBound();
      } else if (type instanceof PsiCapturedWildcardType) {
        PsiType lowerBound = ((PsiCapturedWildcardType) type).getLowerBound();
        type = (lowerBound != NULL ? lowerBound : ((PsiCapturedWildcardType) type).getUpperBound());
      }

      if (type instanceof PsiClassType) {
        PsiClassType classType = (PsiClassType) type;
        Collection<PsiType> typeParams =
            classType.resolveGenerics().getSubstitutor().getSubstitutionMap().values();
        TObjectHashingStrategy<PsiClass> nameComparingHashingStrategy =
            new TObjectHashingStrategy<PsiClass>() {
              @Override
              public int computeHashCode(PsiClass psiClass) {
                return requireNonNull(psiClass.getQualifiedName()).hashCode();
              }

              @Override
              public boolean equals(PsiClass psiClass, PsiClass other) {
                return psiClass.hashCode() == other.hashCode();
              }
            };
        Set<PsiClass> dependencies = new THashSet<>(nameComparingHashingStrategy);
        dependencies.add(toValidPsiClass(classType));
        for (PsiType typeParam : typeParams) {
          if (typeParam
              != null) { // if the user specified raw class such as Map instead of Map<String, String>
            Set<PsiClass> childDependencies = computeDependencies(module, typeParam);
            if (childDependencies == null) {
              return null;
            }
            dependencies.addAll(childDependencies);
          }
        }
        return dependencies;
      }

      throw new IllegalAccessError(
          "Only supports PsiArrayType, PsiPrimitiveType, PsiWildcardType, PsiCapturedWildcardType & PsiClassType. Does not support type: "
              + originalType.getCanonicalText());
    }
    return null;
  }

  @Nullable
  public static PsiClass toValidPsiClass(@NotNull PsiClassType type) {
    if (isValidType(type)) {
      return type.resolve();
    }
    return null;
  }

  // Copied & modified from PsiUtil.ensureValidType
  public static boolean isValidType(@NotNull PsiType type) {
    if (!type.isValid()) {
      TimeoutUtil.sleep(
          1); // to see if processing in another thread suddenly makes the type valid again (which is a bug)
      if (!type.isValid()) {
        return false;
      }
    }
    if (type instanceof PsiArrayType) {
      return isValidType(PsiArrayType.class.cast(type).getComponentType());
    } else if (type instanceof PsiWildcardType) {
      PsiType bound = ((PsiWildcardType) type).getBound();
      return bound != null && isValidType(bound);
    } else if (type instanceof PsiCapturedWildcardType) {
      PsiType lowerBound = ((PsiCapturedWildcardType) type).getLowerBound();
      type = (lowerBound != NULL ? lowerBound : ((PsiCapturedWildcardType) type).getUpperBound());
      return type != NULL && isValidType(type);
    } else if (type instanceof PsiClassType) {
      PsiClassType.ClassResolveResult classResolveResult = ((PsiClassType) type).resolveGenerics();
      return classResolveResult.isValidResult() && isValidElement(
          requireNonNull(classResolveResult.getElement())) && !hasUnresolvedComponents(type);
    }
    return true;
  }

  /**
   * Checks if the element is valid. If not, throws {@link com.intellij.psi.PsiInvalidElementAccessException} with
   * a meaningful message that points to the reasons why the element is not valid and may contain the stack trace
   * when it was invalidated.
   */
  // Copied & modified from PsiUtilCore.ensureValid
  private static boolean isValidElement(@NotNull PsiElement element) {
    if (!element.isValid()) {
      TimeoutUtil.sleep(
          1); // to see if processing in another thread suddenly makes the element valid again (which is a bug)
      return element.isValid();
    }
    return true;
  }

  @Nullable
  public static PsiType getComponentType(@NotNull PsiType type) {
    if (type instanceof PsiArrayType) {
      return ((PsiArrayType) type).getComponentType();
    }
    return extractIterableTypeParameter(type, true);
  }

  @Contract("_, null->false")
  private static boolean representsCollection(@NotNull PsiClass psiClass, @Nullable PsiType type) {
    return type != null && getCollectionItemType(psiClass, type) != null;
  }

  @Nullable
  private static PsiType getCollectionItemType(@NotNull PsiClass psiClass, @NotNull PsiType type) {
    return JavaGenericsUtil.getCollectionItemType(type, psiClass.getResolveScope());
  }

  @Nullable
  public static Map<String, GenericClassMemberWrapper> getSanitisedPropertyToPsiMemberWrapper(
      @Nullable PsiClass psiClass) {
    if (psiClass != null) {
      return getCachedValue(psiClass, SPRING_ASSISTANT_PLUGIN_PROPERTY_TO_CLASS_MEMBER_WRAPPER_KEY,
          () -> create(prepareWritableProperties(psiClass), JAVA_STRUCTURE_MODIFICATION_COUNT));
    }
    return null;
  }

  @NotNull
  private static Map<String, GenericClassMemberWrapper> prepareWritableProperties(
      @NotNull PsiClass psiClass) {
    final Map<String, GenericClassMemberWrapper> memberNameToMemberWrapper = new THashMap<>();
    for (PsiMethod method : psiClass.getAllMethods()) {
      if (method.hasModifierProperty(STATIC) || !method.hasModifierProperty(PUBLIC)) {
        continue;
      }
      if (isSimplePropertyGetter(method)) {
        PsiMember acceptableMember = method;
        final String propertyName = getPropertyName(method);
        assert propertyName != null;

        PsiMethod setter = findInstancePropertySetter(psiClass, propertyName);
        if (setter != null) {
          final PsiType setterArgType = setter.getParameterList().getParameters()[0].getType();
          final PsiField field = psiClass.findFieldByName(propertyName, true);
          if (field != null && !field.hasModifierProperty(STATIC)) {
            final PsiType fieldType = getWritablePropertyType(psiClass, field);
            if (fieldType == null || setterArgType.isConvertibleFrom(fieldType)) {
              acceptableMember = field;
            }
          }
        } else {
          final PsiType returnType = method.getReturnType();
          if (returnType != null && representsCollection(psiClass, returnType)) {
            final PsiField field = psiClass.findFieldByName(propertyName, true);
            if (field != null && !field.hasModifierProperty(STATIC)) {
              final PsiType fieldType = getWritablePropertyType(psiClass, field);
              if (fieldType == null || returnType.isAssignableFrom(fieldType)) {
                acceptableMember = field;
              }
            }
          } else {
            acceptableMember = null;
          }
        }
        if (acceptableMember != null)
          memberNameToMemberWrapper
              .put(sanitise(propertyName), new GenericClassMemberWrapper(acceptableMember));
      }
    }
    return memberNameToMemberWrapper;
  }

  @Nullable
  private static PsiType getWritablePropertyType(@Nullable PsiClass containingClass,
      @Nullable PsiElement declaration) {
    if (declaration instanceof PsiField) {
      return getFieldType((PsiField) declaration);
    }
    if (declaration instanceof PsiMethod) {
      final PsiMethod method = (PsiMethod) declaration;
      if (method.getParameterList().getParametersCount() != 0) {
        return getSetterArgumentType(method);
      }
      final String propertyName = getPropertyName(method);
      final PsiClass psiClass =
          containingClass != null ? containingClass : method.getContainingClass();
      if (propertyName != null && containingClass != null) {
        final PsiMethod setter = findInstancePropertySetter(psiClass, propertyName);
        if (setter != null) {
          final PsiType setterArgumentType = getSetterArgumentType(setter);
          if (setterArgumentType != null)
            return setterArgumentType;
        }
      }
      return getGetterReturnType(method);
    }
    return null;
  }

  @Nullable
  private static PsiType getFieldType(final PsiField field) {
    return getCachedValue(field, SPRING_ASSISTANT_PLUGIN_ERASE_FREE_TYPE_PARAMETER_TYPE_KEY, () -> {
      final PsiType fieldType = field.getType();
      final PsiClassType.ClassResolveResult resolveResult = resolveGenericsClassInType(fieldType);
      final PsiClass fieldClass = resolveResult.getElement();
      if (fieldClass == null) {
        final PsiType propertyType = eraseFreeTypeParameters(fieldType, field);
        return create(propertyType, JAVA_STRUCTURE_MODIFICATION_COUNT);
      }
      return null;
    });
  }

  @Nullable
  private static PsiType getSetterArgumentType(@NotNull PsiMethod method) {
    return getCachedValue(method, SPRING_ASSISTANT_PLUGIN_FIRST_PARAMETER_TYPE_KEY, () -> {
      final PsiParameter[] parameters = method.getParameterList().getParameters();
      if (!method.hasModifierProperty(STATIC) && parameters.length == 1) {
        final PsiType argumentType = eraseFreeTypeParameters(parameters[0].getType(), method);
        return create(argumentType, JAVA_STRUCTURE_MODIFICATION_COUNT);
      }
      return create(null, JAVA_STRUCTURE_MODIFICATION_COUNT);
    });
  }

  @Nullable
  private static PsiType eraseFreeTypeParameters(@Nullable PsiType psiType,
      @NotNull PsiMember member) {
    final PsiClass containingClass = member.getContainingClass();
    return eraseFreeTypeParameters(psiType, containingClass);
  }

  @Nullable
  private static PsiType eraseFreeTypeParameters(@Nullable PsiType psiType,
      @Nullable PsiClass containingClass) {
    if (containingClass == null) {
      return null;
    }
    return getElementFactory(containingClass.getProject()).createRawSubstitutor(containingClass)
        .substitute(psiType);
  }

  private static PsiType getGetterReturnType(@NotNull PsiMethod method) {
    return getCachedValue(method, SPRING_ASSISTANT_PLUGIN_RETURN_TYPE_KEY, () -> {
      final PsiType returnType = eraseFreeTypeParameters(method.getReturnType(), method);
      return create(returnType, JAVA_STRUCTURE_MODIFICATION_COUNT);
    });
  }

  @Nullable
  private static PsiMethod findInstancePropertySetter(@NotNull PsiClass psiClass,
      @Nullable String propertyName) {
    if (StringUtil.isEmpty(propertyName))
      return null;
    final String suggestedSetterName = PropertyUtil.suggestSetterName(propertyName);
    final PsiMethod[] setters = psiClass.findMethodsByName(suggestedSetterName, true);
    for (PsiMethod setter : setters) {
      if (setter.hasModifierProperty(PUBLIC) && !setter.hasModifierProperty(STATIC)
          && isSimplePropertySetter(setter)) {
        return setter;
      }
    }
    return null;
  }

  @Nullable
  public static Module findModule(@NotNull PsiElement element) {
    return findModuleForPsiElement(element);
  }

  @Nullable
  public static Module findModule(@NotNull InsertionContext context) {
    return findModuleForFile(context.getFile().getVirtualFile(), context.getProject());
  }

  @Nullable
  public static String computeDocumentation(PsiMember member) {
    PsiDocComment docComment;
    if (member instanceof PsiField) {
      docComment = PsiField.class.cast(member).getDocComment();
    } else if (member instanceof PsiMethod) {
      docComment = PsiMethod.class.cast(member).getDocComment();
    } else {
      throw new RuntimeException("Method supports targets of type PsiField & PsiMethod only");
    }
    if (docComment != null) {
      StringBuilder builder = new StringBuilder();
      new JavaDocInfoGenerator(member.getProject(), member)
          .generateCommonSection(builder, docComment);
      return builder.toString().trim();
    }
    return null;
  }

  @Nullable
  public static VirtualFile findFileUnderRootInModule(Module module, String targetFileName) {
    VirtualFile[] contentRoots = getInstance(module).getContentRoots();
    for (VirtualFile contentRoot : contentRoots) {
      VirtualFile childFile = findFileUnderRootInModule(contentRoot, targetFileName);
      if (childFile != null) {
        return childFile;
      }
    }
    return null;
  }

  @Nullable
  public static VirtualFile findFileUnderRootInModule(@NotNull VirtualFile contentRoot,
      String targetFileName) {
    VirtualFile childFile = contentRoot.findChild(targetFileName);
    if (childFile != null) {
      return childFile;
    }
    return null;
  }

  /**
   * Debug logging can be enabled by adding fully classified class name/package name with # prefix
   * For eg., to enable debug logging, go `Help > Debug log settings` & type `#in.oneton.idea.spring.assistant.plugin.suggestion.service.SuggestionServiceImpl`
   *
   * @param doWhenDebug code to execute when debug is enabled
   */
  private static void debug(Runnable doWhenDebug) {
    if (log.isDebugEnabled()) {
      doWhenDebug.run();
    }
  }

}
